﻿'''
程序名：2048.py：
功能：Python tkinter + numpy实现 2048游戏
改编人：任吱吱

编程时间：不清楚
网络获取源代码：2048GUI
'''
import numpy as np
import tkinter # GUI 图形用户界面
from tkinter import messagebox  # 单独引入消息窗口库


class Game2048: # 分离游戏规则和算法，单独建立一个类
    def __init__(self): # 游戏界面类初始化-主要是申明参数
        self.data_a = np.zeros((4, 4), dtype=int) # 当前数据阵列
        self._data_old = np.zeros((4, 4), dtype=int) # 上一步数据阵列
#        self._data_old = np.empty(shape=[4, 4]) # 上一步数据阵列
        self.scores = 0 # self.scores = None 游戏得分
        self.steps = 0 # 步数统计数据
        self.step_flag = False # 还没有保存数据，不能回退一步


    def b_move(self,_direction): # 接受到方向事件后的游戏数据处理
        if _direction == 'BackSpace' and self.step_flag == True:
            self.data_a = self._data_old.copy() # 复原上一步数据，真实拷贝数据，不知与*1有什么区别？
            self.step_flag = False
            self.steps -= 1
            return True

        if _direction in ['Up','Down','Left','Right']:
            self._data_old = self.data_a.copy() # 保持上一步数据，真实拷贝数据，不知与*1有什么区别？
        
            if _direction == 'Up': # 启动方向旋转，便于统一数据加工。取消copy()，可利用浅拷贝，免去复原操作！
                _data_tmp = self.data_a.T[::-1] # 复原 self.data_a = _data_tmp[::-1].T.copy()
            elif _direction == 'Down':
                _data_tmp = self.data_a[::-1].T # 复原 self.data_a = _data_tmp.T[::-1].copy()
            elif _direction == 'Right':
                _data_tmp = self.data_a[:,::-1] # 复原 self.data_a = _data_tmp[:,::-1].copy()
            else: # 其它默认'Left'
                _data_tmp = self.data_a # 复原 self.data_a = _data_tmp.copy()

            for _i, data_line in enumerate(_data_tmp): # 逐行分析
                line_temp = [0 for _k in range(4)] # 临时存放行数据
                _d0 = 0 # 临时存放前一个数据，用于判断是否合并
                _k = 0 # 临时结果数据指针回位
                for _j, _d1 in enumerate(data_line):   # 逐个处理
                    if _d1 != 0: # 不为零则执行
                        if _d1 == _d0: # 若与前一个数据相同，合并计分
                            line_temp[_k-1] = _d0 + _d1
                            self.scores += 2*_d0
                            _d0 = 0 # 一次只能完成一组合并
                        else: # 若不同，则直接保存
                            line_temp[_k] = _d1
                            _d0 = _d1
                            _k += 1
                _data_tmp[_i] = line_temp # 完成一行数据处理，保存结果

            if (self.data_a == self._data_old).all(): # 如果全等，则无变动，返回False
                return False
            else: # 如果有变动，返回True
                self.step_flag = True
                self.steps += 1
                return True


    def b_insert(self): # 空挡中随机插入新模块，判断游戏是否结束，更新显示
        zero_pos = np.where(self.data_a == 0) # 寻找所有为0位置
        _rp = np.random.randint(0, len(zero_pos[0])) # 找到填充新数据的随机位置顺序
        pos_0, pos_1 = zero_pos[0][_rp], zero_pos[1][_rp] # 获取随机位置
        self.data_a[pos_0][pos_1] = np.random.choice([2, 4],p = [0.9, 0.1]) # 插入随机数: 2(90%) or 4(10%)


    def game_over(self): # 判断是否有0，或者可合并的模块，即游戏是否结束
        if (self.data_a == 0).any(): # 判断是否有0值？有，则直接返回！
                    return False

        for _i in range(4): # 横向判断是否有可合并模块：相邻且相等
            for _j in range(3): # 最后一列无需判断
                if (self.data_a[_i][_j] == self.data_a[_i][_j+1]): # 只判断下一列数据
                    return False

        for _j in range(4): # 纵向判断是否有可合并模块：相邻且相等
            for _i in range(3): # 最后一行无需判断
                if (self.data_a[_i][_j] == self.data_a[_i+1][_j]): # 只判断下一行数据
                    return False

        return True


class Gui2048: # 游戏界面类，含控制流程
    def __init__(self): # 显示界面类初始化：显示窗体
        self.window = tkinter.Tk() # 创设主窗口
        self.window.title("2048")

        self.window.bind("<Key>", self.key_input) # 绑定按键处理

        self.canvasDa = tkinter.Canvas(self.window, # 规划模块显示区域
                                       width=400,
                                       height=400,
                                       bg="white")
        self.canvasDa.pack()

        frame = tkinter.Frame(self.window) # 创设‘按钮控制’和‘分数显示’框架
        frame.pack()

        self.buttonL = tkinter.Button(frame,  # lambda: 用于在Button控件中传递函数参数
                                      text = "左",
                                      command = lambda: self.d_event('Left')
                                      ).grid(row = 3, column = 2)
        self.buttonR = tkinter.Button(frame,
                                      text="右",
                                      command = lambda: self.d_event('Right')
                                      ).grid(row=3, column=4)
        self.buttonU = tkinter.Button(frame,
                                      text="上",
                                      command = lambda: self.d_event('Up')
                                      ).grid(row=2, column=3)
        self.buttonD = tkinter.Button(frame,
                                      text="下",
                                      command = lambda: self.d_event('Down')
                                      ).grid(row=4, column=3)
        self.buttonD = tkinter.Button(frame,
                                      text="悔",
                                      command = lambda: self.d_event('BackSpace')
                                      ).grid(row=3, column=3)
        
        self.fenshu = tkinter.StringVar()
        tkinter.Label(frame, textvariable = self.fenshu).grid(row=3, column = 5)
        self.bushu = tkinter.StringVar()
        tkinter.Label(frame, textvariable = self.bushu).grid(row=3, column = 1)

        self.label_arrays = [[0 for _i in range(4)] for _j in range(4)] # 设置显示阵列，不能用np.array！
        for _i in range(4): # 初始化4*4显示模块
            for _j in range(4):
                self.label_arrays[_i][_j] = tkinter.Label(self.canvasDa,
                                               width = 10, height = 5,
                                               bg = "white", text = "",
                                               font = 'Helvetica -20 bold')
                self.label_arrays[_i][_j].grid(row = _i + 1, column = _j + 1)


    def key_input(self, event): # 键盘输入判断，转方向事件程序处理
        if event.keysym in ['Up','Down','Left','Right','BackSpace']: # 增加方向按键控制
            self.d_event(event.keysym)


    def b_show(self, _data_a, _scores, _steps): # 按模块阵列更新显示和分数
        for _i, _line in enumerate(_data_a): # 更新模块显示
            for _j, _num in enumerate(_line):
                if _num == 0 : # 空数据变更颜色
                    self.label_arrays[_i][_j]["text"] = ''
                    self.label_arrays[_i][_j]["bg"] = "white"
                else : # 有数据更新数据
                    self.label_arrays[_i][_j]["text"] = _num
                    self.label_arrays[_i][_j]["bg"] = "yellow"
        self.fenshu.set("  分数：%5d"%(_scores)) # 更新分数
        self.bushu.set("  步数：%4d"%(_steps)) # 更新分数


    def d_event(self,_direction): # 根据方向事件，执行游戏进程。实际为主循环
        if _direction=='BackSpace':
            G2048.b_move(_direction)
            self.b_show(G2048.data_a, G2048.scores, G2048.steps) # 显示新模块
        if(G2048.b_move(_direction) == True): # 判断是否有效动作？无效则不作处理
            if _direction in ['Up','Down','Left','Right']:
                G2048.b_insert() # 插入新模块
                self.b_show(G2048.data_a, G2048.scores, G2048.steps) # 显示新模块
            if G2048.game_over() == True: # 判断游戏是否已结束
                messagebox.showwarning('游戏结束！', '最终得分：'+ str(G2048.scores)) 
                if  (messagebox.askyesno('2048游戏', '重新开始游戏？') == True): # 不退出，则重启游戏
                    self.g_start()
                else: # 退出結束
                    self.window.destroy() # 关闭窗口


    def g_start(self): # 游戏起始, 单独列出，为了一轮游戏后的重启！
        G2048.data_a = G2048.data_a * 0 # 恢复当前数据阵列初值
        G2048.scores = 0 # 初始化游戏得分
        G2048.steps = 0 # 初始化步数统计数据
        G2048.step_flag = False # 还没有保存数据，不能回退一步
        G2048.b_insert() # 随机输入一个模块
        self.b_show(G2048.data_a, G2048.scores, G2048.steps) # 显示含有一个模块的初始画面
        

if __name__ == '__main__': # 如果是直接从本程序执行，则启动游戏
    G2048 = Game2048() # 游戏规则类实体化
    GG = Gui2048() # 游戏界面类实体化
    GG.g_start() # 启动初始模块
    GG.window.mainloop() # 进入程序主循环：接收用户指令（按键或按钮）
